<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue defineProperty数据双向绑定</title>
</head>
<body>
    <div id="app">
        <input v-model="name" type="text">
        <div style="margin:20px 0 20px 0;" v-html="name"></div>
        <button v-html="name" @click="changeit">按钮</button>
    </div>
</body>
<script>
    // 负责收集更新函数，属于发布订阅模式中的调度中心
    class Dep {
        constructor() {
            this._listeners = []
        }

        add(obj) {
            this._listeners.push(obj)
        }

        notify() {
            this._listeners.forEach(item => item.update && item.update())
        }
    }
    // 观察者，直接跟DOM，负责将更新函数传递给Dep
    class Watcher {
        constructor(options) {
            this.update = options.update
            Dep.target = this
            this.val = options.allVal[options.key]
            Dep.target = null
        }
    }
    class Vue {
        constructor(options) {
            this.$options = options
            this.$data = options.data()
            // 1.劫持根实例
            this.observerRoot()
            // 2.劫持$data
            this.observerData(this.$data)
            // 3.创建虚拟元素，避免频繁操作真实DOM
            this.createFragment()
            // 4.编译元素，解析指令、事件、方法
            this.compile()
        }
        // 1.劫持根实例
        observerRoot() {
            Object.keys(this.$data).forEach(item => {
                let _val = this[item]
                Object.defineProperty(this, item, {
                    enumerable: true, // 当前属性是否可以遍历
                    configurable: false, //当前属性是否可以再次调用Object.defineProperty
                    get: function () {
                        return this.$data[item]
                    },
                    set: function (newVal) {
                        this.$data[item] = newVal
                    }
                })
            })
        }
        // 2.劫持$data
        observerData(obj) {
            if (!obj || typeof obj != 'object') return
            Object.keys(obj).forEach(item => {
                // TODO: 为什么要引入中间变量
                // 避免进入死循环，如果是this[item]的话，this上的值变化会重新触发set，陷入死循环
                let val = obj[item]
                if (typeof val == 'object') {
                    this.observerData(val)
                } else {
                    let dep = new Dep()
                    Object.defineProperty(obj, item, {
                        enumerable: true,
                        configurable: false,
                        get: function () {
                            // console.log('获取属性')
                            Dep.target && dep.add(Dep.target)
                            return val
                        },
                        set: function (newVal) {
                            // obj[item]代表的是堆里的指针
                            // 而val也是指向堆里的指针，所以修改val的值也就修改了指针
                            // console.log('设置属性')
                            val = newVal
                            dep.notify()
                        }
                    })
                }
            })
        }
        // 3.创建虚拟元素，避免频繁操作真实DOM
        createFragment() {
            this.$el = document.querySelector(this.$options.el)
            this.$flagment = document.createDocumentFragment()
            while (this.$el.firstChild) {
                // appendChild 会把原来$el节点下的所有元素append到$flagment里，此时$el里只有<div id='app'></div>
                this.$flagment.appendChild(this.$el.firstChild)
            }
        }
        // 4.编译元素，解析指令、事件、方法
        compile() {
            // 1.解析
            this._compileElement(this.$flagment)
            // 2.重新appent到梗元素下
            this.$el.appendChild(this.$flagment)
        }
        // 4.1 编译元素，解析指令、事件、方法
        _compileElement(ele) {
            Array.from(ele.childNodes).forEach(item => {
                // 解析节点
                this.__compileNode(item)
                if (item.childNodes) this._compileElement(item)
            })
        }
        // 4.1.1 解析节点
        __compileNode(node) {
            // 1.解析节点包含的指令、事件...
            let res = this.__checkHasBind(node)
            // console.log(res)
            // 2.处理解析结果
            this.__resolveBind(node, res)
        }
        // 4.1.1.1 解析节点包含的指令、事件...
        __checkHasBind(node) {
            let attributes = node.attributes
            let dir_reg = /^v\-\w*$/
            let event_reg = /^\@\w*$/
            let result = {
                directives: [],
                events: []
            }
            if (attributes) Array.from(attributes).forEach(item => {
                if (dir_reg.test(item.name)) result.directives.push({
                    name: item.name,
                    value: item.value
                })
                if (event_reg.test(item.name)) result.events.push({
                    name: item.name,
                    value: item.value
                })
            })
            return result
        }
        // 4.1.1.2 处理单个节点解析结果
        __resolveBind(node, res) {
            // 此处处理好的也是一个对象，包含更新当前节点所需的所有信息
            //     {
            //          allVal: '', // 根实例的$data
            //          key: '', // 当前节点绑定了data上的哪个属性
            //          directive: '', // 当前指令
            //          node: '', // 当前节点
            //          update: () => {} // 如何更新当前节点
            //   }
            let _this = this
            let data = this.$data
            let {
                directives,
                events
            } = res
            directives.length && directives.forEach(item => {
                let update = () => {
                    switch (item.name) {
                        case 'v-model':
                            node.oninput = (val) => {
                                // console.log(val)
                                //vue数据双向绑定精髓
                                _this[item.value] = node.value || ''
                            }
                            node.value = data[item.value]
                            break;
                        case 'v-html':
                            node.innerHTML = data[item.value]
                            break;
                        default:
                            break;
                    }
                }
                // 此处这个配置，没有直接和dep交互。所以是发布订阅模式而不是观察者模式。
                let watch_option = {
                    allVal: data,
                    key: item.value,
                    directive: item.name,
                    node: node,
                    update
                }
                new Watcher(watch_option)
                update()
            })
            // 解析绑定事件
            events.length && events.forEach(item => {
                let method_name = item.value
                // 解析出来的事件，是以@为开头，需要处理
                let target_event = item.name.slice(1, item.name.length)

                node.addEventListener(target_event, () => {
                    this.$options.methods[method_name].call(this)
                })
            })
        }
    }
</script>
<script>
    let app = new Vue({
        el: '#app',
        data() {
            return {
                name: 'xiaoming',
                arr: []
            }
        },
        methods: {
            changeit() {
                this.name = Math.random()
            }
        },
    })
</script>